package cn.gitee.ffmpeg.processed.utils;


import cn.gitee.ffmpeg.processed.dto.WatermarkInfoDTO;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FrameGrabber;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static cn.gitee.ffmpeg.processed.constants.FFmpegConstant.*;


/**
 * @author xuhainan
 * @date 2023/1/17 15:26
 * @region hefei
 */
@Slf4j
public class FFmpegUtil {


    public static String FFMPEG_PATH;

    /**
     * 文件时长 millisecond
     *
     * @param sourcePath 视频路径
     * @return 时长 毫秒
     */
    public static long sourceTime(String sourcePath) {
        ParamVerifyUtil.isFalse(FileUtil.exist(sourcePath), "文件不存在，无法获取时长");
        long duration = 0L;
        try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(sourcePath)) {
            avutil.av_log_set_level(avutil.AV_LOG_ERROR);
            grabber.start();
            duration = grabber.getLengthInTime() / 1000;
            grabber.stop();
        } catch (FrameGrabber.Exception e) {
            throw new RuntimeException(e);
        }
        return duration;

    }

    /**
     * 是否包含音频
     *
     * @param sourcePath 文件地址
     * @return true 包含音频  false 不包含
     */
    public static boolean hasAudio(String sourcePath) {
        ParamVerifyUtil.isFalse(FileUtil.exist(sourcePath), "文件不存在，无法校验是否包含音频");
        boolean hasAudio;
        try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(sourcePath)) {
            avutil.av_log_set_level(avutil.AV_LOG_ERROR);
            grabber.start();
            hasAudio = grabber.hasAudio();
            grabber.stop();
        } catch (FrameGrabber.Exception e) {
            throw new RuntimeException(e);
        }
        return hasAudio;
    }

    /**
     * 重整大小和静音处理
     * 修改分辨率 并 清除音频
     *
     * @param videoPath 视频地址
     * @param weight    宽
     * @param height    高
     * @param outPath   输出路径
     */
    public static boolean reSizeAndMute(String videoPath, long weight, long height, String outPath) {
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);
        commands.add(I);
        commands.add(videoPath);
        commands.add("-vf");
        commands.add("scale=" + weight + ":" + height);
        commands.add("-an");
        commands.add("-q:v");
        commands.add("1");
        commands.add(outPath);

        return execCommand(commands) && FileUtil.exist(outPath);
    }


    /**
     * 音频合并
     *
     * @param audioPaths 音频路径
     * @param resultPath 合并后输出路径
     */
    public static boolean concatAudio(List<String> audioPaths, String resultPath) {
        return concatAudio(audioPaths, resultPath, COPY, null);
    }

    public static boolean concatAudio(List<String> audioPaths, String out, String audioCodec, String videoCodec) {
        log.info("音频合并[concatAudio]开始执行,入参：audio=[{}],out=[{}],audioCodec=[{}],videoCodec=[{}]", audioPaths, out, audioCodec, videoCodec);
        if (CollUtil.isEmpty(audioPaths)) {
            throw new IllegalArgumentException();
        }
        String audioParams = String.join("|", audioPaths);
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add("\"" + CONCAT + ":" + audioParams + "\"");
        if (StrUtil.isNotBlank(audioCodec)) {
            commands.add(A_CODEC);
            commands.add(audioCodec);
        }

        if (StrUtil.isNotBlank(videoCodec)) {
            commands.add(V_CODEC);
            commands.add(videoCodec);
        }
        commands.add(NOSTDIN);
        commands.add(out);
        log.info("音频合并[concatAudio]执行命令：" + String.join(" ", commands));
        boolean execCommand = execCommand(commands);
        log.info("音频合并[concatAudio]结束：audio=[{}],out=[{}],audioCodec=[{}],videoCodec=[{}],result=[{}]",
                audioPaths, out, audioCodec, videoCodec, execCommand);
        return execCommand && FileUtil.exist(out);
    }

    public static boolean concatAudio0(List<String> audioPaths, String out) {
        log.info("音频合并[concatAudio0]开始执行,入参：audio=[{}],out=[{}]", audioPaths, out);
        if (CollUtil.isEmpty(audioPaths)) {
            throw new IllegalArgumentException();
        }
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        StringBuilder builder = new StringBuilder();
        int size = audioPaths.size();
        for (int i = 0; i < size; i++) {
            commands.add("-i");
            commands.add(audioPaths.get(i));
            builder.append("[").append(i).append(":a]");
        }
        builder.append("concat=n=").append(size).append(":v=0:a=1:unsafe=1[out]");

        commands.add("-filter_complex");
        commands.add(builder.toString());

        commands.add("-map");
        commands.add("[out]");
        commands.add("-c:a");
        commands.add("libmp3lame");
        commands.add(NOSTDIN);
        commands.add(out);
        log.info("音频合并[concatAudio0]执行命令：" + String.join(" ", commands));
        boolean result = execCommand(commands) && FileUtil.exist(out);
        log.info("音频合并[concatAudio0]结束：audio=[{}],out=[{}],result=[{}]", audioPaths, out, result);
        return result && FileUtil.exist(out);
    }

    /**
     * 生成空白音频
     *
     * @param time 音频时间
     * @param out  输出路径
     */
    public static boolean processEmptyAudio(String time, String out) {
        log.info("生成空白音频入参：time=[{}],out=[{}]", time, out);
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add("-f");
        commands.add("lavfi");

        commands.add(I);
        commands.add("aevalsrc=0");
        commands.add("-t");
        commands.add(time);
        commands.add(A_CODEC);
        commands.add("libmp3lame");
        commands.add(out);
        log.info("生成空白音频 command > [{}]", String.join(" ", commands));
        boolean execCommand = execCommand(commands);
        log.info("生成空白音频结束：time=[{}],out=[{}],result=[{}]", time, out, commands);
        return execCommand && FileUtil.exist(out);
    }


    /**
     * 替换视频中的音频
     *
     * @param video 视频路径
     * @param audio 音频路径
     * @param out   处理后的视频路径
     */
    public static boolean videoReplaceAudio(String video, String audio, String out) {
        log.info("替换视频中的音频入参：audio=[{}],video=[{}],out=[{}]", audio, video, out);
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add(video);
        commands.add(I);
        commands.add(audio);
        commands.add("-map");
        commands.add("0:0");
        commands.add("-map");
        commands.add("1:0");
        commands.add(V_CODEC);
        commands.add(COPY);
        commands.add(out);
        log.info("替换视频中的音频 command > [{}]", String.join(" ", commands));
        boolean execCommand = execCommand(commands);
        log.info("替换视频中的音频结束：audio=[{}],video=[{}],out=[{}],result=[{}]", audio, video, out, execCommand);
        return execCommand && FileUtil.exist(out);
    }

    /**
     * 剪切音频
     *
     * @param audio    音频
     * @param start    开始时间
     * @param duration 持续时间
     * @param out      输出音频
     */
    public static boolean cutAudio(String audio, String start, String duration, String out) {
        log.info("剪切音频入参：audio=[{}],start=[{}],duration=[{}],out=[{}]", audio, start, duration, out);
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);
        commands.add(I);
        commands.add(audio);
        commands.add("-ss");
        commands.add(start);
        commands.add("-t");
        commands.add(duration);

        commands.add(A_CODEC);
        commands.add(COPY);
        commands.add(out);
        log.info("剪切音频 command > [{}]", String.join(" ", commands));
        boolean execCommand = execCommand(commands);
        log.info("剪切音频结束：audio=[{}],start=[{}],duration=[{}],out=[{}],result=[{}]", audio, start, duration, out, execCommand);
        return execCommand && FileUtil.exist(out);
    }

    /**
     * 分割视频
     *
     * @param video    视频路径
     * @param start    开始时间
     * @param duration 持续时间
     * @param end      结束时间
     * @param out      分割后的视频路径
     */
    public static boolean cutVideo(String video, String start, String duration, String end, String out) {
        log.info("分割视频入参：video=[{}],start=[{}],duration=[{}],end=[{}],out=[{}]", video, start, duration, end, out);
        FileUtil.del(out);
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);


        commands.add("-ss");
        commands.add(start);

        commands.add(I);
        commands.add(video);

        if (StrUtil.isNotBlank(duration)) {
            commands.add("-t");
            commands.add(duration);
        }

        if (StrUtil.isNotBlank(end)) {
            commands.add("-to");
            commands.add(end);
        }
        commands.add(V_CODEC);
        commands.add(COPY);

        commands.add(out);

        log.info("分割视频 command > [{}]", String.join(" ", commands));
        boolean execCommand = execCommand(commands);
        log.info("分割视频结束：video=[{}],start=[{}],duration=[{}],end=[{}],out=[{}],result=[{}]", video, start, duration, end, out, execCommand);
        return execCommand && FileUtil.exist(out);
    }

    /**
     * 合并两个视频
     *
     * @param videoList  视频集合
     * @param outputFile 输出地址
     */
    public static boolean mergeVideo(List<String> videoList, String outputFile, String prefix) {
        log.info("合并两个视频入参：video=[{}],out=[{}]", videoList, outputFile);
        if (CollUtil.size(videoList) != 2) {
            throw new IllegalArgumentException("视频内容不匹配，期望两个");
        }
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);
        commands.add(I);
        String one = videoList.get(0);
        commands.add(one);

        commands.add(I);
        String two = videoList.get(1);
        commands.add(two);

        commands.add("-filter_complex");
        commands.add("[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1");
        commands.add(outputFile);
        log.info("合并两个视频 command > [{}]", String.join(" ", commands));
        boolean execCommand = execCommand(commands);
        log.info("合并两个视频结束：video=[{}],out=[{}],result=[{}]", videoList, outputFile, execCommand);
        return execCommand && FileUtil.exist(outputFile);
    }

    /**
     * 合并视频
     *
     * @param videoList  视频集合
     * @param outputFile 输出路径
     * @param prefix     前缀
     * @return 路径
     */
    public static boolean mergeVideoWithTxt(List<String> videoList, String outputFile, String prefix) {
        log.info("合并视频[mergeVideoWithTxt]入参：video=[{}],out=[{}],prefix=[{}]", videoList, outputFile, prefix);
        if (StrUtil.isBlank(outputFile)) {
            throw new IllegalArgumentException("输出文件路径不能为空");
        }
        FileUtil.del(outputFile);
        // 写入文件中
        String tempText = obtainParentFolder(outputFile) + prefix + obtainFileName(outputFile) + ".txt";
        writeVideoToTextFile(true, tempText, videoList);
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add("-f");
        commands.add(CONCAT);
        commands.add("-safe");
        commands.add("0");
        commands.add(I);
        commands.add(tempText);
        commands.add("-c");
        commands.add(COPY);
        commands.add(NOSTDIN);

        commands.add(outputFile);
        log.info("合并视频[mergeVideoWithTxt] command > [{}]", String.join(" ", commands));
        boolean flag = false;
        if (execCommand(commands)) {
            // 删除临时的合成txt文件
            FileUtil.del(tempText);
            flag = FileUtil.exist(outputFile);
        }
        log.info("合并视频[mergeVideoWithTxt]结束：video=[{}],out=[{}],prefix=[{}],result=[{}]", videoList, outputFile, prefix, flag);
        return flag;
    }


    /**
     * 添加背景音乐
     *
     * @param video     源视频
     * @param backMusic 背景音乐
     * @param out       输出视频
     */
    public static boolean addBackMusic(String video, String backMusic, String out) {
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add(video);
        commands.add(I);
        commands.add(backMusic);

        commands.add("-filter_complex");
        commands.add("\"amix=inputs=2:duration=first:dropout_transition=2\"");

        commands.add("-c:v");
        commands.add(COPY);
        commands.add(out);

        return execCommand(commands) && FileUtil.exist(out);
    }

    /**
     * 视频与音频合并
     * 混音处理，视频带音频
     *
     * @param video 视频地址
     * @param audio 音频地址
     * @param out   输出文件
     */
    public static boolean mergeAudioToVideo(String video, String audio, String out) {
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add(video);
        commands.add(I);
        commands.add(audio);

        commands.add("-filter_complex");
        commands.add("[0][1]amix=inputs=2[mix]");
        commands.add("-map");
        commands.add("0:v");
        commands.add("-c:v");
        commands.add(COPY);
        commands.add("-map");
        commands.add("[mix]");
        commands.add("-shortest");
        commands.add(out);
        return execCommand(commands) && FileUtil.exist(out);
    }

    /**
     * 视频与音频合并
     * 视频须无音频
     *
     * @param video 视频地址
     * @param audio 音频地址
     * @param out   输出文件
     */
    public static boolean mergeAudioToVideo0(String video, String audio, String out) {
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add(video);
        commands.add(I);
        commands.add(audio);

        commands.add("-c:v");
        commands.add(COPY);
        commands.add("-c:a");
        commands.add(COPY);

        commands.add("-shortest");
        commands.add(out);
        return execCommand(commands) && FileUtil.exist(out);
    }


    public static boolean mergeAudioToVideo(String video, Map<String, Integer> audios, String out) {
        return mergeAudioToVideo(video, new ArrayList<>(audios.keySet()), out, (List<Integer>) audios.values());
    }

    /**
     * 视频中指定时间添加音频
     * 音频列表与时间列表长度相同
     *
     * @param video      视频
     * @param audios     音频列表
     * @param out        输出音频
     * @param delayTimes 指定时间集合
     */
    public static boolean mergeAudioToVideo(String video, List<String> audios, String out, List<Integer> delayTimes) {
        if (CollUtil.isEmpty(audios) || CollUtil.isEmpty(delayTimes) || CollUtil.size(audios) != CollUtil.size(delayTimes)) {
            throw new IllegalArgumentException("audios or delay param is error");
        }
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add(video);

        for (String audio : audios) {
            commands.add(I);
            commands.add(audio);
        }

        String format = "[%s]adelay=%s[aud%s];";
        int delayTimeLength = delayTimes.size();
        StringBuilder bu = new StringBuilder();
        StringBuilder end = new StringBuilder("[0]");
        for (int i = 0; i < delayTimeLength; i++) {
            Integer time = delayTimes.get(i);
            bu.append(String.format(format, (i + 1), time, (i + 1)));
            end.append("[aud").append(i + 1).append("]");
        }
        end.append("amix=").append(delayTimeLength + 1);
        bu.append(end);

        commands.add("-filter_complex");
        commands.add(bu.toString());

        commands.add("-c:v");
        commands.add(COPY);

        commands.add(out);

        return execCommand(commands) && FileUtil.exist(out);
    }


    /**
     * 将要合并的avi写入txt文件(格式：file '/path/to/yourfile/video-00001.ts')，单个写入，每行一个
     *
     * @param textFilePath 合成视频中间txt文件
     */
    public static void writeVideoToTextFile(boolean isCreateNew, String textFilePath, List<String> videoList) {
        File file = FileUtil.file(textFilePath);
        try {
            boolean creator = true;
            if (isCreateNew) {
                if (!file.exists()) {
                    FileUtil.mkParentDirs(file);
                    creator = file.createNewFile();
                } else {
                    if (file.delete()) {
                        FileUtil.mkParentDirs(file);
                        creator = file.createNewFile();
                    }
                }
            }
            if (!creator) {
                throw new IllegalArgumentException("文件创建失败");
            }
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true)));
            for (String video : videoList) {
                out.write(FILE + " '" + video + "'\r\n");
                out.flush();
            }
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 添加水印
     *
     * @param dto 水印参数
     * @return 生成状态
     */
    public static boolean watermarkVideo(WatermarkInfoDTO dto) {
        String sourcePath = dto.getSourcePath();
        String targetPath = dto.getTargetPath();
        List<WatermarkInfoDTO.FontInfo> fontInfos = dto.getFontInfos();
        List<String> commands = ListUtil.of(FFMPEG_PATH, REWRITER);

        commands.add(I);
        commands.add(sourcePath);

        commands.add("-vf");

        StringBuilder builder = new StringBuilder();
        for (WatermarkInfoDTO.FontInfo fontInfo : fontInfos) {
            String content = fontInfo.getContent();
            String drawTextFormat = "drawtext=fontfile='SourceHanSansSC-Bold.otf':fontsize=%s:x=%s:y=H-th-%s:fontcolor=%s:shadowcolor='#000000@0.2':shadowy=1:text='%s'";
            String drawText = String.format(drawTextFormat, fontInfo.getFontSize(), fontInfo.getXLocation(), fontInfo.getYLocation(), fontInfo.getFontColor(), content);
            builder.append(drawText);
        }
        commands.add(builder.toString());
        commands.add("-b:v");
        commands.add("0");
        commands.add(V_CODEC);
        commands.add("h264");
        commands.add(NOSTDIN);
        commands.add(targetPath);

        return execCommand(commands) && FileUtil.exist(targetPath);
    }


    // -----------------------------Base--------------------------------
    public static String obtainParentFolder(String path) {
        String separator = path.contains("\\") ? "\\" : "/";
        return StrUtil.subBefore(path, separator, true) + separator;
    }

    public static String obtainFileName(String path) {
        String replacePath = path.replace("\\", "/");
        return StrUtil.subBefore(StrUtil.subAfter(replacePath, "/", true), ".", true);
    }


    private static boolean execCommand(List<String> commands) {
        return execCommand(commands.toArray(new String[0]));
    }

    private static boolean execCommand(String... commands) {
        String command = String.join(" ", commands);
        try {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            ProcessBuilder pb = new ProcessBuilder(commands);
            pb.inheritIO();
            pb.directory(new File(System.getProperty("user.home")));
            Process process = pb.start();
            int exitCode = process.waitFor();
            process.destroy();
            stopWatch.stop();
            log.info("[command] [{}] 执行结果waitFor {} ,执行时长：{}s", command, exitCode, stopWatch.getTotalTimeSeconds());
            if (exitCode == 0) {
                System.out.println("Process exited successfully");
            } else {
                System.out.println("Process exited with error code " + exitCode);
                BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                String line;
                while ((line = errorReader.readLine()) != null) {
                    System.out.println(line);
                }
            }
            return exitCode == 0;
        } catch (Exception e) {
            log.error("执行命令[{}]异常", command, e);
            return false;
        }
    }
}
